package net.sudot.commons.security;

import com.fasterxml.jackson.core.type.TypeReference;
import net.sudot.chess.business.dto.ErrorResponse;
import net.sudot.chess.exception.BusinessException;
import net.sudot.commons.json.JsonUtils;
import net.sudot.commons.security.event.UserLoggedInEvent;
import net.sudot.commons.utils.WebUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.pam.UnsupportedTokenException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.SavedRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.annotation.PostConstruct;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * 认证过滤器
 *
 * @author tangjialin on 2017-06-14 0014.
 */
public class AuthenticationFilter extends FormAuthenticationFilter {

    protected Logger logger = LoggerFactory.getLogger(getClass());

    /** 用户类型 */
    private Class<?> userClass;
    /** 允许访问的用户类型集合 */
    private Set<Class<?>> accessAllowedUserClass;
    private ApplicationEventPublisher applicationEventPublisher;

    public AuthenticationFilter(Class<?> userClass) {
        this.userClass = userClass;
    }

    @PostConstruct
    public void postConstruct() {
        if (getAccessAllowedUserClass() == null) {
            setAccessAllowedUserClass(new HashSet<>());
        }
        if (getUserClass() != null) {
            getAccessAllowedUserClass().add(getUserClass());
        }
    }

    /**
     * 创建令牌
     *
     * @param servletRequest  ServletRequest
     * @param servletResponse ServletResponse
     * @return 令牌
     */
    @Override
    protected org.apache.shiro.authc.AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
        String contentType = servletRequest.getContentType();
        MediaType mediaType = contentType == null ? null : MediaType.parseMediaType(contentType);
        if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {
            try (InputStream inputStream = servletRequest.getInputStream()) {
                Map<String, String> user = JsonUtils.toObject(inputStream, new TypeReference<Map<String, String>>() {
                });
                String username = user.get(getUsernameParam());
                String password = user.get(getPasswordParam());
                return createToken(username, password, servletRequest, servletResponse);
            } catch (IOException e) {
                throw new IllegalArgumentException();
            }
        }
        return createToken(servletRequest, servletResponse);
    }

    /**
     * 创建令牌
     *
     * @param username   登录用户账户
     * @param password   登录用户密码
     * @param rememberMe 是否记住登录状态
     * @param host       登录人主机信息
     * @return 令牌
     */
    @Override
    protected AuthenticationToken createToken(String username, String password, boolean rememberMe, String host) {
        return new UserAuthenticationToken(getUserClass(), username, password, rememberMe, host);
    }

    /**
     * 是否允许访问
     *
     * @param servletRequest  ServletRequest
     * @param servletResponse ServletResponse
     * @param mappedValue     映射值
     * @return 是否允许访问
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) {
        Subject subject = getSubject(servletRequest, servletResponse);
        Object principal = subject.getPrincipal();
        if (principal != null && !getAccessAllowedUserClass().contains(principal.getClass())) {
            return false;
        }
        return super.isAccessAllowed(servletRequest, servletResponse, mappedValue);
    }

    /**
     * 访问被拒绝处理
     * <p>
     * 访问被拒绝后,会根据情况执行{@link AuthenticationFilter#redirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
     *
     * @param servletRequest  ServletRequest
     * @param servletResponse ServletResponse
     * @return 是否继续处理
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        return super.onAccessDenied(servletRequest, servletResponse);
    }

    /**
     * 登录成功处理
     *
     * @param authenticationToken 令牌
     * @param subject             Subject
     * @param servletRequest      ServletRequest
     * @param servletResponse     ServletResponse
     * @return 返回true:进入控制器,返回false:结束登录请求
     */
    @Override
    protected boolean onLoginSuccess(org.apache.shiro.authc.AuthenticationToken authenticationToken, Subject subject, ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        // 发布用户登录成功事件
        applicationEventPublisher.publishEvent(new UserLoggedInEvent(this, subject.getPrincipal()));
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (WebUtils.isResponseJson(request)) {
            // 返回true,进入控制器
            return true;
        }
        return super.onLoginSuccess(authenticationToken, subject, servletRequest, servletResponse);
    }

    /**
     * 登录失败处理
     *
     * @param authenticationToken     令牌
     * @param authenticationException 认证异常
     * @param servletRequest          ServletRequest
     * @param servletResponse         ServletResponse
     * @return 是否继续处理
     */
    @Override
    protected boolean onLoginFailure(org.apache.shiro.authc.AuthenticationToken authenticationToken, AuthenticationException authenticationException, ServletRequest servletRequest, ServletResponse servletResponse) {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (!WebUtils.isResponseJson(request)) {
            return super.onLoginFailure(authenticationToken, authenticationException, servletRequest, servletResponse);
        }
        HttpStatus httpStatus = HttpStatus.UNPROCESSABLE_ENTITY;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(httpStatus.value());

        ErrorResponse errorResponse = new ErrorResponse(httpStatus, request.getRequestURI());
        if (authenticationException instanceof UnknownAccountException) {
            errorResponse.setMessage("账号或密码错误");
        } else if (authenticationException instanceof LockedAccountException) {
            errorResponse.setMessage("账号已锁定");
        } else if (authenticationException instanceof DisabledAccountException) {
            errorResponse.setMessage("账号已禁用");
        } else if (authenticationException instanceof IncorrectCredentialsException) {
            errorResponse.setMessage("账号或密码错误");
        } else if (authenticationException instanceof UnsupportedTokenException) {
            errorResponse.setMessage("不支持的登录类型");
        } else {
            if (authenticationException.getCause() instanceof BusinessException) {
                errorResponse.setMessage(authenticationException.getCause().getMessage());
            } else {
                logger.error("登陆异常", authenticationException);
                errorResponse.setMessage("登陆失败");
            }
        }
        try (PrintWriter writer = response.getWriter()) {
            JsonUtils.toJson(writer, errorResponse);
        } catch (IOException e) {
            logger.error("响应错误", e);
        }
        return false;
    }

    /**
     * 重定向到登录操作
     * <p>
     * 访问被拒绝后,如果不是访问的登录页面会进入此函数,详见{@link FormAuthenticationFilter#onAccessDenied(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
     *
     * @param servletRequest  ServletRequest
     * @param servletResponse ServletResponse
     * @throws IOException
     */
    @Override
    protected void redirectToLogin(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (WebUtils.isResponseJson(request)) {
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            Subject subject = getSubject(servletRequest, servletResponse);
            HttpStatus httpStatus = subject.isAuthenticated() ? HttpStatus.FORBIDDEN : HttpStatus.UNAUTHORIZED;
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setStatus(httpStatus.value());
            ErrorResponse errorResponse = new ErrorResponse(httpStatus, request.getRequestURI());
            errorResponse.setMessage(subject.isAuthenticated() ? "你没有足够的权限执行此操作" : "你可能太长时间没有操作了, 出于安全考虑, 系统拒绝了你的请求。");
            try (PrintWriter writer = response.getWriter()) {
                JsonUtils.toJson(writer, errorResponse);
            }
        } else {
            super.redirectToLogin(servletRequest, servletResponse);
        }
    }

    /**
     * 登录成功之后会执行此方法进行登录后的操作
     *
     * @param servletRequest  ServletRequest
     * @param servletResponse ServletResponse
     * @throws Exception
     */
    @Override
    protected void issueSuccessRedirect(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (WebUtils.isResponseJson(request)) {
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            String redirect = Optional.ofNullable(org.apache.shiro.web.util.WebUtils.getAndClearSavedRequest(request))
                    .filter(savedRequest -> AccessControlFilter.GET_METHOD.equalsIgnoreCase(savedRequest.getMethod()))
                    .map(SavedRequest::getRequestUrl)
                    .orElse(request.getContextPath() + getSuccessUrl());
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setStatus(HttpStatus.OK.value());
            response.setHeader(HttpHeaders.LOCATION, redirect);
        } else {
            super.issueSuccessRedirect(servletRequest, servletResponse);
        }
    }

    @Override
    protected String getHost(ServletRequest request) {
        return WebUtils.getRequestFullIp((HttpServletRequest) request);
    }

    /**
     * 获取用户类型
     *
     * @return 用户类型
     */
    public Class<?> getUserClass() {
        return userClass;
    }

    /**
     * 设置用户类型
     *
     * @param userClass 用户类型
     */
    public AuthenticationFilter setUserClass(Class<?> userClass) {
        this.userClass = userClass;
        return this;
    }

    /**
     * 获取允许访问的用户类型集合
     *
     * @return 允许访问的用户类型集合
     */
    public Set<Class<?>> getAccessAllowedUserClass() {
        return accessAllowedUserClass;
    }

    /**
     * 设置允许访问的用户类型集合
     *
     * @param accessAllowedUserClass 允许访问的用户类型集合
     */
    public AuthenticationFilter setAccessAllowedUserClass(Set<Class<?>> accessAllowedUserClass) {
        this.accessAllowedUserClass = accessAllowedUserClass;
        return this;
    }

    public ApplicationEventPublisher getApplicationEventPublisher() {
        return applicationEventPublisher;
    }

    public AuthenticationFilter setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
        return this;
    }
}
